/*
 * @(#)NateOnChannel.java
 *
 * Copyright (c) 1998-2005 Jang-Ho Hwang, 
 * All Rights Reserved.
 *
 * $Id: NateOnChannel.java,v 1.1.1.1 2005/05/22 01:02:27 rath Exp $
 */
package rath.nateon;

import java.io.*;
import java.lang.reflect.Method;
import java.net.Socket;
import java.net.SocketException;
import java.net.InetSocketAddress;
import java.util.HashMap;
/**
 * 네이트온 메신져 클래스
 * 
 * @author Jang-Ho Hwang, rath@xrath.com
 * @version 1.0.000, 2005/05/20
 */
public abstract class NateOnChannel extends Thread
{
	private static final Class[] PARAM = new Class[] { NateOnMessage.class };
	private static final Class[] PARAM_OBJ = new Class[] { NateOnMessage.class, Object.class };

	private InetSocketAddress addr;
	protected Socket sock;
	protected InputStream in;
	protected OutputStream out;
	protected boolean isLive = true;
	private HashMap callbackMap = new HashMap();

	private ByteArrayOutputStream buf = new ByteArrayOutputStream();

	private int trId = 1;

	protected NateOnChannel( String addr )
	{
		int i0 = addr.indexOf(':');
		if( i0==-1 )
			throw new IllegalArgumentException("Address format is invalid");
		
		String host = addr.substring(0, i0);
		int port = Integer.parseInt(addr.substring(i0+1));
		setTargetAddress( new InetSocketAddress(host, port) );
	}

	protected NateOnChannel( String host, int port )
	{
		this( new InetSocketAddress(host, port) );
	}

	protected NateOnChannel( InetSocketAddress addr )
	{
		setTargetAddress( addr );
	}

	public final void run()
	{
		int status = 0;
		try
		{
			connect();
			status = 1;
			channelConnected();
	
			// i/o 예외를 제외한 것들은 그저 무시할 수 있도록 하자.
			while(isLive)
			{
				NateOnMessage msg = readMessage();
				if( msg==null )
					break;
				if( msg.getHeader().equals("PING") || msg.getHeader().equals("PONG") )
				{
				}
				else
				{
					if( Debug.printInput )
						System.out.println( "=> " + msg );				
				}

				try
				{
					Object cb = callbackMap.get(new Integer(msg.getTrId()));
					Method m = null;
					if( cb!=null && cb instanceof Method )
					{
						m = (Method)cb;
						m.invoke( this, new Object[] { msg } );
					}
					else
					if( cb!=null )
					{
						m = (Method)((Object[])cb)[0];
						Object param = ((Object[])cb)[1];
						m.invoke( this, new Object[] { msg, param } );
					}

					if( cb==null )
					{
						processMessage( msg );
					}
				}
				catch( RuntimeException e )
				{
					// FIXME: File에 로그를 팍 남겨버리자.	
					e.printStackTrace();
				}
			}			
		}
		catch( Exception e )
		{
			channelError( e );
			if( status==0 )
			{
				// connect error
				connectFailed(e);
			}
		}
		finally
		{
			cleanUp();
			if( Debug.printInput )
				System.out.println( "* Disconnected" );

			if( status > 0 )
				channelClosed();
		}
	}

	private void setTargetAddress( InetSocketAddress addr )
	{
		this.addr = addr;
	}

	/**
	 * 이 채널이 연결된 목적지의 주소를 얻어온다.
	 */
	public InetSocketAddress getTargetAddress()
	{
		return this.addr;
	}

	/**
	 * 목적 서버로의 연결을 맺는다.
	 */
	protected void connect() throws IOException
	{
		this.sock = new Socket();
		this.sock.connect( addr );

		this.in = sock.getInputStream();
		this.out = sock.getOutputStream();
	}

	/**
	 * 메시지 하나를 읽어들인다.
	 */
	public NateOnMessage readMessage() throws IOException
	{
		buf.reset();
		boolean isCR = false;
		while(true)
		{
			int v = in.read();
			if( v==-1 )
				break;
			if( !isCR && v=='\r' )
			{
				isCR = true;
				continue;
			}
			if( isCR && v=='\n' )
				break;
			buf.write( v );
		}

		if( buf.size()==0 )
			return null;

		byte[] b = buf.toByteArray();
		return NateOnMessage.create(b);
	}

	public byte[] readBytes( int len ) throws IOException
	{
		byte[] b = new byte[ len ];
		int off = 0;
		while(off < b.length )
		{
			int readlen = in.read(b, off, b.length-off);
			if( readlen==-1 )
				break;
			off += readlen;
		}
		return b;
	}

	/**
	 * 메시지를 보낸다.
	 */
	public void writeMessage( NateOnMessage msg ) throws IOException
	{
		msg.setTrId( trId );
		if( msg!=null && (msg.getHeader().equals("PING") || msg.getHeader().equals("PONG")) )
		{

		}
		else
		{
			if( Debug.printOutput )
				System.out.println( "<= " + msg );
		}

		if( msg.getCallback()!=null )
		{
			String methodName = msg.getCallback();
			Object param = msg.getCallbackObject();
			try
			{
				if( param==null )
				{
					callbackMap.put( new Integer(trId), 
						this.getClass().getDeclaredMethod(methodName, PARAM) );
				}
				else
				{
					callbackMap.put( new Integer(trId), 
						new Object[] { 
						this.getClass().getDeclaredMethod(methodName, PARAM_OBJ), param} );
				}
			}
			catch( NoSuchMethodException e ) 
			{
				System.err.println( "Cannot find method: " + methodName + ": " + e );
			}
		}

		trId++;
		out.write( msg.getBytes() );
	}

	public void writeBytes( byte[] b ) throws IOException
	{
		out.write( b );
	}

	public void cleanUp()
	{
		if( in!=null )
		{
			try
			{
				in.close();
			}
			catch( IOException e ) {}
		}
		if( out!=null )
		{
			try
			{
				out.close();
			}
			catch( IOException e ) {}
		}
		if( sock!=null )
		{
			try
			{
				sock.close();
			}
			catch( IOException e ) {}
		}
	}

	protected abstract void connectFailed( Exception e );

	protected abstract void channelConnected() throws IOException;

	protected abstract void channelClosed();

	protected abstract void processMessage( NateOnMessage msg ) throws IOException;

	protected void channelError( Exception e )
	{
		if( !(e instanceof SocketException) )
			e.printStackTrace();
	}
}
